/*
 * Copyright (c) 2010 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Name: BlacklistEditor.java
 *
 * Description: Command line application to display and manipulate the Query
 * Suggest service's Blacklist (words & phrases that won't be suggested to the
 * user when using the Query Suggest service).
 *
 * The class uses the Google Admin API for java. This requires the appliance to
 * be at least of software version 6.0. It will read or set the Query Suggest
 * blacklist information from the specified GSA.
 *
 * Please note that this class needs the following GData libraries:
 * - gdata-core-1.0.jar
 * - gdata-gsa-1.0.jar
 * - google-collect1.0-rc1.jar
 *
 * The libraries can be downloaded from:
 * http://code.google.com/p/google-enterprise-gdata-api/
 *
 * This class also needs the following Apache Commons library:
 * - commons-cli-1.2.jar
 *
 * This library can be downloaded from:
 * http://commons.apache.org/cli/download_cli.cgi
 *
 * To run the program use the following two commands:
 * 1) From the project's top level directory (the directory that contains the
 *   'src' folder) compile the project by running
 *   ant deploy
 *
 * 2) execute
 *   java -jar ble.jar -h gsa1 -u admin -p secret -l
 *
 * Please note that the parameter settings are just examples. You will have to
 * replace them with your local appliance values.
 * usage: java -jar ble.jar [options]
 *  -a,--add <word>          add <word> to the blacklist
 *  -h,--hostname <host>     the IP address or hostname of the GSA
 *  -i,--import <file>       import <file> to overwrite the blacklist
 *  -l,--list                list all blacklisted terms
 *  -P,--port <portNum>      the port number of the GSA's admin interface.
 *                           If not provided, defaults to 8000.
 *  -p,--password <pw>       the user's password.  If not provided, the user
 *                           will be prompted for one.
 *  -r,--remove <word>       remove <word> from the blacklist
 *  -u,--username <userid>   an administrator's username
 *
 * Examples:
 * * Prints all the terms in the query suggest blacklist
 *   java -jar ble.jar --hostname=gsa.mydomain.com --port=8000 --username=admin \
 *   --password=myPassw0rd list
 *
 * * Removes 'blue' from the query suggest blacklist
 *   java -jar ble.jar --hostname=gsa.mydomain.com --port=8000 --username=admin \
 *   --password=myPassw0rd remove blue
 *
 * * Replaces all the current blacklisted terms with the terms in myTerms.txt.
 *   java -jar ble.jar --hostname=gsa.mydomain.com --port=8000 --username=admin \
 *   --password=myPassw0rd import myTerms.txt
 *
 * * Adds 'blue ink' to the query suggest blacklist.
 *   java -jar ble.jar --hostname=gsa.mydomain.com --port=8000 --username=admin \
 *   --password=myPassw0rd add "blue ink"
 */
package com.google.enterprise.gsa.suggest.blacklist;

import com.google.enterprise.apis.client.GsaEntry;
import com.google.enterprise.apis.client.ImprovedGsaClient;
import com.google.enterprise.apis.client.ImprovedGsaClientFactory;
import com.google.enterprise.gsa.util.PasswordPrompt;
import com.google.gdata.util.AuthenticationException;
import com.google.gdata.util.ServiceException;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.OptionBuilder;
import org.apache.commons.cli.OptionGroup;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;

import static java.lang.System.err;
import static java.lang.System.exit;
import static java.lang.System.out;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.net.MalformedURLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * The BlacklistEditor class enables easy manipulation of the GSA's Query
 * Suggest list.  As of the current GSA software (version 6.2), the GSA's
 * Admin API is the only way to add or remove terms that are not supposed to
 * appear in the Query Suggest drop down on GSA front ends.
 * <br><br>
 * This class also includes a command line interface.  Run the command without
 * any options for usage instructions.
 *
 * @author Google
 */
public class BlacklistEditor {

  private static final String GSA_ADMIN_API_SERVICE_ERROR = "Error with GSA Admin API service.";
  private static final String AN_IO_ERROR_HAS_OCCURRED = "An IO Error has occurred.";
  private static final String BLACKLIST_ENTRYID = "suggestBlacklist";
  private static final String SUGGEST_FEED = "suggest";

  /*
   * The character that terms are separated with natively
   */
  private static final char TERM_DELIMITER = '\n';

  /*
   * BlacklistEditor CLI instructions
   */
  private static final char ADD_OPT =  'a';
  private static final char IMP_OPT = 'i';
  private static final char LIST_OPT = 'l';
  private static final char PASS_OPT = 'p';
  private static final char PORT_OPT = 'P';
  private static final char REMV_OPT = 'r';
  private static final char USER_OPT = 'u';
  private static final char HOST_OPT = 'h';

  private static OptionGroup command;

  /*
   * Instance variable(s)
   */
  private ImprovedGsaClient gsaClient;

  /**
   * Creates a new instance from a pre-existing {@code ImprovedGsaClient}
   * object.
   *
   * @param client A pre-constructed {@code GsaClient}
   * @see ImprovedGsaClient
   * @see ImprovedGsaClientFactory
   */
  public BlacklistEditor(ImprovedGsaClient client) {
    gsaClient = client;
  } // End constructor

  /**
   * Creates a new instance using the supplied parameters.
   *
   * @param hostname The GSA to connect to
   * @param port The GSA's management port
   * @param user The user to log in as
   * @param password The {@code user}'s password
   * @throws AuthenticationException if there is problem logging in
   */
  public BlacklistEditor(String hostname, int port, String user,
      String password) throws AuthenticationException {
    gsaClient = ImprovedGsaClientFactory.create(hostname, port, user, password);
  } // End constructor

  /**
   * Gets all of the Query Suggest's blacklisted terms including duplicates.
   *
   * @return All blacklisted terms in no particular order; {@code null} only
   * if there has been an error retrieving the terms.
   * @throws IOException
   * @throws ServiceException
   * @throws MalformedURLException
   */
  public String[] getBlacklistTerms() throws MalformedURLException, ServiceException, IOException{
    String rawterms = getRawBlacklist();
    if (rawterms == null) {
      return null;
    }

    return rawterms.split(String.valueOf(TERM_DELIMITER));
  } // End getBlacklistTerms

  /**
   * Appends a term to the Query Suggest blacklist.
   *
   * @param term The term to add to the blacklist
   * @return A {@link GsaEntry} corresponding to data successfully updated in the server
   * @throws IOException
   * @throws ServiceException
   * @throws MalformedURLException
   */
  public GsaEntry addBlacklistTerm(String term)
  throws MalformedURLException, ServiceException, IOException {
    String currentBL = getRawBlacklist();
    // TODO(todd.re...): check if the word is already in the list
    return overwriteBlacklist(currentBL + term + TERM_DELIMITER);
  } // End addBlacklistTerm(string)

  /**
   * Removes all instances of the term from the blacklist.
   *
   * @param term the string to remove from the blacklist
   * @return The number of times this string was removed from the list.
   * @throws IOException
   * @throws ServiceException
   * @throws MalformedURLException
   */
  public int removeBlacklistTerm(String term)
  throws MalformedURLException, ServiceException, IOException {
    boolean success = true;
    int numberOfRemovals = 0;

    ArrayList<String> allTerms = new ArrayList<String>(Arrays.asList(getBlacklistTerms()));
    do {
      success = allTerms.remove(term);
      if (success) {
        numberOfRemovals++;
      }
    } while (success);

    overwriteBlacklist(allTerms);
    return numberOfRemovals;

  } // End removeBlacklistTerm(string)

  /**
   * A convenience method for {@link #importBlacklist(Reader)}
   *
   * @param blackListFilename the file to import
   * @return A GsaEntry corresponding to data successfully updated in the server
   * @throws ServiceException
   * @throws IOException
   * @throws MalformedURLException
   * @see #importBlacklist(Reader)
   */
  public GsaEntry importBlacklist(String blackListFilename)
  throws ServiceException, MalformedURLException, IOException, FileNotFoundException {

    FileReader reader = null;
    reader = new FileReader(blackListFilename);

    GsaEntry entry = null;
    try {
      entry = importBlacklist(reader);
    } finally {
      reader.close();
    }
    return entry;
  } // End importBlacklist(string)

  /**
   * Imports the blacklist from the given reader.  The reader will not be 'closed' by this method.
   * @param blackListReader the file to import
   * @return A GsaEntry corresponding to data successfully updated in the server
   * @throws IOException
   * @throws ServiceException
   * @throws MalformedURLException
   */
  public GsaEntry importBlacklist(Reader blackListReader)
  throws MalformedURLException, ServiceException, IOException {

    BufferedReader reader = new BufferedReader(blackListReader);

    String line = null;
    ArrayList<String> terms = new ArrayList<String>();

    //
    // Currently, empty file is ok.  Used to reset the list
    //
    while ((line = reader.readLine()) != null) {
      terms.add(line);
    }

    return overwriteBlacklist(terms);
  } // End importBlacklist(file)

  /**
   * A helper utility that takes a list of strings and concatenates them
   * together separated by a delimiter.
   *
   * @param listOfStrings the strings to concatenate
   * @param delim the delimiter that will separate the strings
   * @return The newly formed concatenated string.
   */
  private String stringJoin(List<String> listOfStrings, char delim){
    StringBuffer strbuf = new StringBuffer();
    for (String str : listOfStrings){
      strbuf.append(str).append(delim);
    }
    return strbuf.toString();
  } // End stringJoin(list<string>, char)

  /**
   * Replaces the Query Suggest blacklist with the string provided.
   *
   * @param theList the new blacklist string.
   * @return A {@link GsaEntry} corresponding to data successfully updated in the server
   * @throws IOException
   * @throws ServiceException
   * @throws MalformedURLException
   */
  private GsaEntry overwriteBlacklist(String theList)
  throws MalformedURLException, ServiceException, IOException {
    GsaEntry entry = new GsaEntry();
    entry.addGsaContent(BLACKLIST_ENTRYID, theList);
    return gsaClient.updateEntry(SUGGEST_FEED, BLACKLIST_ENTRYID, entry);
  } // End overwriteBlacklist(string)

  /**
   * Replaces the Query Suggest blacklist with the strings provided.
   *
   * @param listOfStrings the new Query Suggest blacklist strings
   * @return A {@link GsaEntry} corresponding to data successfully updated in the server
   * @throws IOException
   * @throws ServiceException
   * @throws MalformedURLException
   */
  private GsaEntry overwriteBlacklist(List<String> listOfStrings)
  throws MalformedURLException, ServiceException, IOException{
    return overwriteBlacklist(stringJoin(listOfStrings, TERM_DELIMITER));
  } // End overwriteBlacklist(list<string>)

  /**
   * Gets the Query Suggest blacklist in its raw form.
   * As of version 6.2 of the GSA software this is a '\n' delimited string.
   *
   * @return The Query Suggest blacklist string; {@code null} if the list
   * could not be retrieved.
   * @throws IOException
   * @throws ServiceException
   * @throws MalformedURLException
   */
  private String getRawBlacklist() throws MalformedURLException, ServiceException, IOException {
    GsaEntry entry = gsaClient.getEntry(SUGGEST_FEED, BLACKLIST_ENTRYID);

    if (entry == null) {
      return null;
    }

    return entry.getGsaContent(BLACKLIST_ENTRYID);
  } // End getRawBlacklist


  /*========================================================================*/
  /*|         Command Line / Static Stuff Below here                       |*/
  /*========================================================================*/

  /**
   * Start the program here.
   *
   * @param args Command Line arguments
   */
  public static void main(String[] args) {
    Options options = setUpOptions();
    CommandLineParser parser = new PosixParser();

    String username = null;
    String password = null;
    String hostname = null;
    int port = 8000;

    //
    // Set the variables from the CLI arguments
    //
    CommandLine line = null;
    try {
      line = parser.parse(options, args);
    } catch (ParseException e) {
      err.println(e.getMessage());
      HelpFormatter formatter = new HelpFormatter();
      formatter.printHelp("java -jar ble.jar [options]", options);
      exit(1);
    }

    username = line.getOptionValue(USER_OPT);
    hostname = line.getOptionValue(HOST_OPT);

    //
    // Get the optional password from the CL
    //
    if (line.hasOption(PASS_OPT)) {
      password = line.getOptionValue(PASS_OPT);
    } else {
      password = PasswordPrompt.getPasswordViaConsole("Enter password for " + username + ": ");
    }

    //
    // Get the optional port from the CL
    //
    if (line.hasOption(PORT_OPT)) {
      port = Integer.parseInt(line.getOptionValue(PORT_OPT));
    }

    ImprovedGsaClient client = null;
    try {
      client = ImprovedGsaClientFactory.create(hostname, port,
          username, password);

    } catch (AuthenticationException e) {
      err.println(e.getMessage());
      exit(1);
    }

    BlacklistEditor ble = new BlacklistEditor(client);
    char selected = command.getSelected().charAt(0); // should probably check for null

    switch (selected) {
    case LIST_OPT:
      String[] terms = null;
      try {
        terms = ble.getBlacklistTerms();
      } catch (MalformedURLException e) {
        err.println("Could not get blacklist terms.");
        printErrorAndExit(e, 1);
      } catch (ServiceException e) {
        err.println(GSA_ADMIN_API_SERVICE_ERROR);
        printErrorAndExit(e, 1);
      } catch (IOException e) {
        err.println(AN_IO_ERROR_HAS_OCCURRED);
        printErrorAndExit(e, 1);
      }
      assert terms != null;
      for (String term : terms) {
        out.println(term);
      }
      out.println("\n*** End of List ***");
      break;

    case REMV_OPT:
      int removals = 0;

      String remTerm = line.getOptionValue(REMV_OPT);
      try {
        if ((removals = ble.removeBlacklistTerm(remTerm)) > 0) {
          out.println("Successfully removed " + removals +
              " instances of '" + remTerm + "' from the blacklist.");
        } else {
          out.println("Could not find '" + remTerm + "' in the blacklist.");
        }
      } catch (MalformedURLException e) {
        err.println("Could not remove '" + remTerm + "' from the blacklist.");
        printErrorAndExit(e, 1);
      } catch (ServiceException e) {
        err.println(GSA_ADMIN_API_SERVICE_ERROR);
        err.println("Could not remove '" + remTerm + "' from the blacklist.");
        printErrorAndExit(e, 1);
      } catch (IOException e) {
        err.println(AN_IO_ERROR_HAS_OCCURRED);
        err.println("Could not remove '" + remTerm + "' from the blacklist.");
        printErrorAndExit(e, 1);
      }
      break;

    case ADD_OPT:
      String addTerm = line.getOptionValue(ADD_OPT);

      //
      // Ignoring return value for now.
      //
      try {
        ble.addBlacklistTerm(addTerm);
        out.println("Successfully added '" + addTerm + "' to the blacklist.");
      } catch (MalformedURLException e) {
        err.println("Unable to add '" + addTerm + "' to the blacklist.");
        printErrorAndExit(e, 1);
      } catch (ServiceException e) {
        err.println(GSA_ADMIN_API_SERVICE_ERROR);
        err.println("Unable to add '" + addTerm + "' to the blacklist.");
        printErrorAndExit(e, 1);
      } catch (IOException e) {
        err.println(AN_IO_ERROR_HAS_OCCURRED);
        err.println("Unable to add '" + addTerm + "' to the blacklist.");
        printErrorAndExit(e, 1);
      }
      break;

    case IMP_OPT:
      String impFile = line.getOptionValue(IMP_OPT);
      try {
        ble.importBlacklist(impFile);
        out.println("Blacklist successfully imported.");
      } catch (MalformedURLException e) {
        err.println("Error importing blacklist.");
        printErrorAndExit(e, 1);
      } catch (FileNotFoundException e) {
        err.println("Could not find '" + impFile + "'.");
        printErrorAndExit(e, 1);
      } catch (ServiceException e) {
        err.println(GSA_ADMIN_API_SERVICE_ERROR);
        printErrorAndExit(e, 1);
      } catch (IOException e) {
        err.println(AN_IO_ERROR_HAS_OCCURRED);
        printErrorAndExit(e, 1);
      }
      break;

    default:
      break;
    }

    exit(0);
  } // End main

  /*
   * Sets all the CLI flags and options (and their defaults if appropriate).
   */
  @SuppressWarnings("static-access")
  private static Options setUpOptions() {
    Options opts = new Options();
    opts.addOption(OptionBuilder.withArgName("pw").hasArg()
        .withLongOpt("password").withDescription("the user's password.  " +
            "If not provided, the user will be prompted for one.").create(PASS_OPT));
    opts.addOption(OptionBuilder.withArgName("host").hasArg().isRequired()
        .withLongOpt("hostname").withDescription("the IP address or hostname of the GSA")
        .create(HOST_OPT));
    opts.addOption(OptionBuilder.withArgName("userid").hasArg().isRequired()
        .withLongOpt("username").withDescription("an administrator's username")
        .create(USER_OPT));
    opts.addOption(OptionBuilder.withArgName("portNum").hasArg()
        .withLongOpt("port").withDescription("the port number of the GSA's " +
            "admin interface.  If not provided, defaults to 8000.")
        .create(PORT_OPT));

    //
    // Create the mutually exclusive option group for a command
    //
    command = new OptionGroup();
    command.setRequired(true);
    command.addOption(OptionBuilder.withLongOpt("list")
        .withDescription("list all blacklisted terms").create(LIST_OPT));
    command.addOption(OptionBuilder.withArgName("word").hasArg()
        .withLongOpt("add").withDescription("add <word> to the blacklist")
        .create(ADD_OPT));
    command.addOption(OptionBuilder.withArgName("word").hasArg()
        .withLongOpt("remove").withDescription("remove <word> from the blacklist")
        .create(REMV_OPT));
    command.addOption(OptionBuilder.withArgName("file").hasArg()
        .withLongOpt("import").withDescription("import <file> to overwrite " +
            "the blacklist").create(IMP_OPT));

    opts.addOptionGroup(command);
    return opts;
  }

  private static void printErrorAndExit(Exception e, int exitCode) {
    err.println(e.getLocalizedMessage());
    exit(exitCode);
  }
} // End BlacklistEditor
